import datetime
import functools
import json
import logging
import threading
import uuid
from abc import ABC, abstractmethod
from typing import Optional

from flask import current_app, g, request

from .ctx import get_request_id
from .exception import ignore_exception

_local = threading.local()


# @pysnooper.snoop(depth=2)
class AbstractEventLogger(ABC):
    @abstractmethod
    def log(self, user_id, action, duration_ms, *args, **kwargs):
        pass

    def log_this(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            user_id = None
            if g.user:
                user_id = g.user.get_id()
            d = request.form.to_dict() or {}

            # request parameters can overwrite post body
            request_params = request.args.to_dict()
            d.update(request_params)
            d.update(kwargs)

            d.update(request.json or {})

            self.stats_logger.incr(f.__name__)
            start_dttm = datetime.datetime.now()
            value = f(*args, **kwargs)
            duration_ms = (datetime.datetime.now() - start_dttm).total_seconds() * 1000

            if user_id:
                self.log(user_id, f.__name__, duration_ms=duration_ms, **d)
            return value

        return wrapper

    @property
    def stats_logger(self):
        return current_app.config.get('STATS_LOGGER')


# @pysnooper.snoop(depth=2)
class DBEventLogger(AbstractEventLogger):
    def log(self, user_id, action, duration_ms, *args, **kwargs):
        from myapp.models.log import Log

        referrer = request.referrer[:1000] if request.referrer else None

        log = Log(
            action=action,
            json=json.dumps(kwargs, indent=4, ensure_ascii=False),
            duration_ms=duration_ms,
            referrer=referrer,
            user_id=user_id,
            method=request.method,
            path=request.path,
            dttm=datetime.datetime.now(),
        )

        sesh = current_app.appbuilder.get_session
        sesh.add(log)
        sesh.commit()


class JsonFormatter(logging.Formatter):
    def __init__(
            self,
            fmt: Optional[str] = '%(asctime)s',
            datefmt: Optional[str] = None,
            style: Optional[str] = '%',
    ) -> None:
        super().__init__(fmt=fmt, datefmt=datefmt, style=style)

    def formatMessage(self, record: logging.LogRecord, *args, **kwargs) -> str:
        super().formatMessage(record)

        user_id = None
        request_id = get_request_id()

        with ignore_exception:
            user_id = g.user_id

        upstream_service_name = ''
        with ignore_exception:
            header_upstream_service_name = request.headers.get('upstream-service-name')
            if header_upstream_service_name:
                upstream_service_name = header_upstream_service_name

        task_id = None
        with ignore_exception:
            task_id = _local.task_id

        data = {
            'time': record.asctime,
            'level': record.levelname,
            'message': record.message,
            'user_id': user_id,
            'x_request_id': request_id,
            'logger': record.name,
            'log_path': f'{record.pathname}:{record.lineno}',
            'source': 0,
            'upstream_service_name': upstream_service_name,
        }

        if task_id is not None:
            data['task_id'] = task_id

        exclude_fields = [
            'name',
            'msg',
            'args',
            'levelname',
            'levelno',
            'pathname',
            'filename',
            'module',
            'exc_info',
            'exc_text',
            'stack_info',
            'lineno',
            'funcName',
            'created',
            'msecs',
            'relativeCreated',
            'thread',
            'threadName',
            'processName',
            'process',
            'asctime',
        ]
        for k, v in record.__dict__.items():
            if k not in exclude_fields:
                data[k] = v

        return json.dumps(data, ensure_ascii=False)


def set_task_id(request_id=None):
    if request_id is None:
        request_id = str(uuid.uuid4())
    _local.task_id = request_id
